[Previous] [Next]

ASP Components

As you know, ASP scripts can instantiate and use ActiveX components, which can add tremendous flexibility and power to ASP scripts.

Using Components in ASP Scripts

There are two ways to instantiate ActiveX components in ASP scripts: by using the Server.CreateObject method and by using an <OBJECT> tag with the SCOPE attribute set to server. The former technique is more likely to appeal to Visual Basic programmers, whereas the latter will sound more natural to HTML programmers.

In at least one case, however, it makes sense even for Visual Basic programmers to use an <OBJECT> tagùnamely, to create an object reference that has application scope or session scope. Let's say that you want to create an ADO Connection object that is shared by all the scripts in the session. You can achieve this by creating the object in the Session_OnStart event procedure and then storing the reference in a Session variable:

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Session_OnStart()
    ' Create the ADO Connection object.
    Set conn = Server.CreateObject("ADODB.Connection")
    ' Open it.
    connStr = "Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=Pubs"
    conn.Open connStr, "sa", "myPwd" 
    ' Make it available to all ASP scripts.
    Set Session("conn") = conn
End Sub
</SCRIPT>

An ASP script can use this session-scoped Connection object, but it has to extract it from the Session object:

<%  ' Inside an ASP script
    Set conn = Session("conn")
    conn.BeginTrans               %>

Let's see what happens when the object is declared in Global.asa using a <SCRIPT> tag with a proper SCOPE attribute:

<OBJECT RUNAT=server SCOPE=Session ID="Conn" PROGID="ADODB.Connection">
</OBJECT>
<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Session_OnStart()
    ' Open the connection (no need to create it).
    connStr = "Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=Pubs"
    conn.Open connStr, "sa", "myPwd" 
End Sub
</SCRIPT>

When an object is declared in this way, you can reference it from any session in the application just by using its name, as in the following ASP script:

<%  conn.BeginTrans %>

Objects can be defined this way with Application scope as well as with Session scope. In both cases, they appear in the StaticObjects collection of the corresponding object.

NOTE
Most components designed for ASP pages are in-process components. From time to time, however, you might need to create out-of-process components. To do so, you must manually modify the AllowOutOfProcCmpts value in the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\ASP \Parameters Registry key from 0 (the default value) to 1.

Using Custom ASP Components

You can use any ActiveX component from an ASP page, including those written in Visual Basic. For example, you can write a component that augments VBScript in the areas in which this script language is weak, such as file management, fast math calculations, string routines, and so on. These components can't really be classified as ASP components, however, because they don't interact with the ASP object model. What we need is a component that can read data coming from an HTML form through the Request object and write data using the Response object.

Writing ASP components in Visual Basic

Writing an ASP component in Visual Basic is surprisingly simpleùexcept for one detail, it's exactly like writing a standard ActiveX component. The first thing you do is start an ActiveX DLL project, set the threading model to Apartment model, and check the Unattended Execution option in the General tab of the Project Properties dialog box.

Visual Basic 6 offers a new option for components marked for unattended execution, the Retained In Memory flag. (See Figure 20-10.) When this option is enabled, the component is held in memory until the client process terminates. This capability is especially useful when you expect that your component will often be loaded in memory and then discarded because it saves Windows the overhead of continuously loading it from disk. When the component is running inside IIS or MTS, which typically serve hundreds or even thousands of clients, this option is going to speed up the overall performance noticeably.

Figure 20-10. The suggested Project settings for a typical ASP component.

Now you have to add a reference to the ASP type library. Two such libraries are registered in a system on which IIS has been installed: the Microsoft Active Server Pages Object Library and the Microsoft Active Server Pages 2.0 ObjectContext Class Type Library. The former library includes the five main ASP objects, and the latter includes only the definition of the ObjectContext object, which you need only when developing ASP components that must run under MTS. Both type libraries are contained in the Asp.dll file.

As you see, up to this point there isn't really anything special about an ASP component. The only problem left to be resolved is this: How can the component get a reference to one of the five main ASP objects? Well, the script code might pass such an object to a property or a method of the component, soon after creating it, but this technique isn't recommended and isn't even necessary if you know the little secret about writing ASP components in Visual Basic.

As soon as the component is created by an ASP script, IIS invokes the component's OnStartPage method, if the component exposes such a method. Therefore the only thing to do is to add the code for this method:

' This is a class-level variable.
Dim sc As ASPTypeLibrary.ScriptingContext

Sub OnStartPage(AspSC As ASPTypeLibrary.ScriptingContext)
    ' Save the reference for later.
    Set sc = AspSC
End Sub

The ScriptingContext object passed to the OnStartPage method is nothing but the root object of the ASP type library. A quick trip to the object browser reveals that this object exposes five propertiesùApplication, Request, Response, Server, and Sessionùwhich are just the main elements of the ASP object model. It is therefore simple to set or retrieve a Session or Application variable or send HTML text by using the Response.Write method:

' Inside the component
Sub IncrementCounter(CounterName As String)
    sc.Application.Lock
    sc.Application(CounterName) = Application(CounterName) + 1
    sc.Application.Unlock
End Sub

When the page that instantiated the component is about to be unloaded, the component receives an OnEndPage event, in which you close the connection and release the resources that you allocated in the OnStartPage event. But you'd usually use the Terminate event instead.

A real, useful component

Now that you're nearing the end of this huge book about Visual Basic programming, you're ready for something more complex than an unsophisticated Hello-World-like ASP component. On the companion CD, you'll find the complete source code for the ASPSample.QueryToTable component, which accepts a connection string and a query string and automatically builds an HTML table that contains the result of the query against the specified data source. It even supports alignment and formatting on a field-by-field basis.

Before describing the source code, let me show you how you can use this custom component from an ASP script:

<%    
Set tbl = Server.CreateObject("ASPSample.QueryToTable")
' Enter the next two lines as a single VBScript statement.
conn = "Provider=SQLOLEDB;Data Source=MyServer;" &
    "Initial Catalog= Pubs;UserID=sa;Password=MyPwd"
tbl.Execute conn, "SELECT * FROM Authors WHERE State = 'CA'"
tbl.GenerateHTML
%>

It couldn't be simpler! The Execute method expects the connection string and the text of the SQL query, and the GenerateHTML method sends the generated HTML text to the page being built. You can fine-tune the format of the output table by using the component's ShowRecNumbers property (set it to True to display record numbers in the leftmost column) and AddField method, which lets you decide which fields appear in the table, the horizontal and vertical alignment attributes of the corresponding cells in the table, and the formatting of their values. The syntax of AddField method follows:

AddField FldName, Caption, HAlign, VAlign, PrefixTag, PostfixTag

To display a field using default options, you just need to pass the field's name:

<%  tbl.AddField "au_lname"
    tbl.AddField "au_fname"    %>

You can specify the caption of the column header (if it's different from the field's name) and the horizontal and vertical alignment attributes of the table cells like this:

<%  tbl.AddField "au_lname", "Last Name", "center", "middle"
    tbl.AddField "au_fname", "First Name", "center", "middle"    %>

Finally, you can format the cells by using the PrefixTag and PostfixTag arguments, as here:

<%  ' Display the State field using boldface characters.
    tbl.AddField "State", , "center", , "<B>", "</B>"
    ' Display the ZIP field using boldface and italic attributes.
    tbl.AddField "ZIP", , "center", , "<B><I>", "</I></B>"       %>

The component doesn't validate the last two arguments; you must ensure that the tags you're passing form a valid HTML sequence. The field layout that you set with a sequence of AddField methods is preserved across consecutive queries, but you can clear the current layout by using the ResetFields method.

Implementing the component

Now that you know what the component does, understanding how its source code works shouldn't be too difficult. The component holds all the information about the columns to be displayed in the Fields array of UDTs. The AddField method does nothing but store its arguments in this array. If the script calls the Execute method without first calling the AddField method, the component builds a default field layout.

' Public Properties ------------------------
' True if Record numbers must be displayed
Public ShowRecNumbers As Boolean

' Private Members ---------------------------
Private Type FieldsUDT
    FldName As String
    Caption As String
    HAlign As String
    VAlign As String
    PrefixTag As String
    PostfixTag As String
End Type

' A reference to the ASP library entry point
Dim sc As ASPTypeLibrary.ScriptingContext
' The Recordset being opened
Dim rs As ADODB.Recordset
' Array information about the fields
Dim Fields() As FieldsUDT
' Number of elements in the Fields array
Dim FieldCount As Integer

When the component is instantiated by an ASP script, its OnStartPage method is called. In this method, the component stores a reference to the ASPTypeLibrary.ScriptingContext object and initializes the Fields array:

' This event fires when the component is instantiated
' from within the ASP script.
Sub OnStartPage(AspSC As ASPTypeLibrary.ScriptingContext)
    ' Save the reference for later.
    Set sc = AspSC
    ResetFields
End Sub

' Reset the field information.
Sub ResetFields()
    Dim Fields(0) As FieldsUDT
    FieldCount = 0
End Sub

The Execute method is just a wrapper around the ADO Recordset's Open method:

' Execute an SQL query.
Function Execute(conn As String, sql As String)
    ' Execute the query.
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenStatic, adLockReadOnly
End Function

The AddField method does a minimal validation of its arguments and stores them in the first available element in the Fields array:

' Add a field to the table layout.
Sub AddField(FldName As String, Optional Caption As String, _
    Optional HAlign As String, Optional VAlign As String, _
    Optional PrefixTag As String, Optional PostfixTag As String)
    ' Check the values.
    If FldName = "" Then Err.Raise 5

    ' Add to the internal array.
    FieldCount = FieldCount + 1
    ReDim Preserve Fields(0 To FieldCount) As FieldsUDT
    With Fields(FieldCount)
        .FldName = FldName
        .Caption = Caption
        .HAlign = HAlign
        .VAlign = VAlign
        .PrefixTag = PrefixTag
        .PostfixTag = PostfixTag
        
        ' The default caption is the field's name.
        If .Caption = "" Then .Caption = FldName
        
        ' The default horizontal alignment is "left."
        Select Case LCase$(.HAlign)
            Case "left", "center", "right"
            Case Else
                .HAlign = "left"
        End Select
        .HAlign = " ALIGN=" & .HAlign
        
        ' The default vertical alignment is "top."
        Select Case LCase$(.VAlign)
            Case "top", "middle", "bottom"
            Case Else
                .VAlign = "top"
        End Select
        .VAlign = " VALIGN=" & .VAlign
    End With
End Sub

The heart of the QueryToTable component is the GenerateHTML method, which uses the contents of the Recordset and the layout information held in the Fields array to build the corresponding HTML table. Although this code might seem complex at first, it took me just a few minutes to build it. Notice that I obtained cleaner code by using a private Send procedure, which actually sends the HTML code to the Response object:

' Generate the HTML text for the table.
Sub GenerateHTML()
    Dim i As Integer, recNum As Long, f As FieldsUDT
    
    ' Initialize the Fields array if not done already.
    If FieldCount = 0 Then InitFields
    ' Restart from the first record.
    rs.MoveFirst
    
    ' Output the table header and the border.
    Send "<TABLE BORDER=1>"
    Send "  <THEAD>"
    Send "   <TR>"
    ' Insert a column for the record number, if requested.
    If ShowRecNumbers Then
        Send "    <TH ALIGN=Center>Rec #</TH>"
    End If
    ' These are the fields' captions.
    For i = 1 To UBound(Fields)
        f = Fields(i)
        Send "    <TH" & f.HAlign & ">" & f.Caption & "</TH>"
    Next
    Send "   </TR>"
    Send "  </THEAD>"
    Send " <TBODY>"
    
    ' Output the body of the table.
    Do Until rs.EOF
        ' Add a new row of cells.
        Send "  <TR>"
        ' Add the record number if requested.
        recNum = recNum + 1
        If ShowRecNumbers Then
            Send "   <TD ALIGN=center>" & recNum & "</TD>"
        End If
        
        ' Send all the fields of the current record.
        For i = 1 To UBound(Fields)
            f = Fields(i)
            Send "   <TD" & f.HAlign & f.VAlign & ">" & f.PrefixTag & _
                rs(f.FldName) & f.PostfixTag & "</TD>"
        Next
        Send "  </TR>"
        ' Advance to the next record.
        rs.MoveNext
    Loop
        
    ' Close the table.
    Send " </TBODY>"
    Send "</TABLE>"
End Sub

' Send a line of text to the output stream.
Sub Send(Text As String)
    sc.Response.Write Text
End Sub

' Initialize the Fields() array with suitable values.
Private Sub InitFields()
    Dim fld As ADODB.Field
    ResetFields
    For Each fld In rs.Fields
        AddField fld.Name
    Next
End Sub

On the companion CD, you'll find the complete source code for this component and a Test.asp page that uses it. The great thing about writing ASP components in Visual Basic 6 is that you can debug them without having to compile them to a DLL. This is a little magic that the Visual Basic IDE does for us: IIS believes that the script is executing an in-process DLL while you're comfortably testing it in the environment, using the full range of debugging tools that Visual Basic provides. (See Figure 20-11.) An example of a table produced by the component is shown in Figure 20-12. I encourage you to augment the component's versatility by adding other properties and methods, for example, to control cell color, value formatting, and so on.

Click to view at full  size.

Figure 20-11. Add a breakpoint in the OnStartPage procedure, and then press F8 to single-step through the component's source code as the ASP script invokes its methods.

Click to view at full  size.

Figure 20-12. An HTML table built by the sample component. Notice that you can refine and reexecute the query by entering text in the controls in the upper portion of the page.